package com.google.code.netcheckin.io;

import java.io.IOException;
import java.io.Reader;
import java.text.MessageFormat;
import java.text.ParseException;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.TreeSet;

import net.sourceforge.jbird.io.DelimitedExtractor;

import com.google.code.netcheckin.aprs.AprsFormatter;
import com.google.code.netcheckin.aprs.Station;
import com.google.code.netcheckin.util.StationCallCollator;

/**
  * Use this class to read station information from a CSV file.
  * Data are returned as an Object[] of Stations.  Typical use follows.
  * <ul>
  * <li>Instantiate.</li>
  * <li>Read.</li>
  * <li>Close.</li>
  * </ul>
  * The array of stations will be sorted by call sign, by default by the 
  * complete call sign, or optionally by call sign suffix. To sort by suffix, 
  * pass to the read method a 
  * com.google.code.netcheckin.util.StationCallSuffixCollator object.
  * <p>
  * The first line of the CSV file should contain column names given in
  * COL_NAMES. (See StationDbWriter for a main method that will print out
  * a comma-separated list of column names.) The columns can be in any order. 
  * Extraneous columns will be ignored.  Use double quotes to enclose values
  * that contain commas.
  * <p>
  * Any error in parsing data values in a line causes the line (and station)
  * to be dropped. After an error, the remaining columns are parsed to check
  * for additional errors.
  * @author Dick Repasky
  *
*/

public class StationDbReader {

	public static final int DELIMITER = ',';

	public static final String[] COL_NAMES = new String[] {
		"CallSign",
		"Name",
		"RACES",
		"ARES",
		"CellPhone",
		"e-mail",
		"PrimaryDescription",
		"PrimaryAddress",
		"PrimaryCity",
		"PrimaryState",
		"PrimaryZip",
		"PrimaryLatDegrees",
		"PrimaryLatMinutes",
		"PrimaryLatDirection",
		"PrimaryLongDegrees",
		"PrimaryLongMinutes",
		"PrimaryLongDirection",
		"PrimaryIcon",
		"SecondaryDescription",
		"SecondaryAddress",
		"SecondaryCity",
		"SecondaryState",
		"SecondaryZip",
		"SecondaryLatDegrees",
		"SecondaryLatMinutes",
		"SecondaryLatDirection",
		"SecondaryLongDegrees",
		"SecondaryLongMinutes",
		"SecondaryLongDirection",
		"SecondaryIcon",
		"Comment",
	};

	public static final int	IDX_CALL_SGN		= 0;
	public static final int	IDX_NAME		= 1;
	public static final int	IDX_RACES		= 2;
	public static final int	IDX_ARES		= 3;
	public static final int	IDX_CELLPHONE		= 4;
	public static final int	IDX_EMAIL		= 5;
	public static final int	IDX_PRIM_DESCR		= 6;
	public static final int	IDX_PRIM_ADDR		= 7;
	public static final int	IDX_PRIM_CITY		= 8;
	public static final int	IDX_PRIM_STATE		= 9;
	public static final int	IDX_PRIM_ZIP		= 10;
	public static final int	IDX_PRIM_LAT_DEG	= 11;
	public static final int	IDX_PRIM_LAT_MIN	= 12;
	public static final int	IDX_PRIM_LAT_DIR	= 13;
	public static final int	IDX_PRIM_LONG_DEG	= 14;
	public static final int	IDX_PRIM_LONG_MIN	= 15;
	public static final int	IDX_PRIM_LONG_DIR	= 16;
	public static final int	IDX_PRIM_ICON		= 17;
	public static final int	IDX_SEC_DESCR		= 18;
	public static final int	IDX_SEC_ADDR		= 19;
	public static final int	IDX_SEC_CITY		= 20;
	public static final int	IDX_SEC_STATE		= 21;
	public static final int	IDX_SEC_ZIP		= 22;
	public static final int	IDX_SEC_LAT_DEG		= 23;
	public static final int	IDX_SEC_LAT_MIN		= 24;
	public static final int	IDX_SEC_LAT_DIR		= 25;
	public static final int	IDX_SEC_LONG_DEG	= 26;
	public static final int	IDX_SEC_LONG_MIN	= 27;
	public static final int	IDX_SEC_LONG_DIR	= 28;
	public static final int	IDX_SEC_ICON		= 29;
	public static final int	IDX_COMMENT		= 30;

	protected static final String ERROR_MSG =
	   "Error: bad value in line {0} {1} ({2}) - dropped";
	protected static final Object[] ERROR_DATA = new Object[3];

	protected DelimitedExtractor extractor;
		/** Input column assignments. The value for each element
	          * is the index of column in the input data that contains
		  * the field.
		*/
	protected int[] colId = new int[COL_NAMES.length];;

	protected AprsFormatter formatter;

	public StationDbReader(Reader r) {
		extractor = new DelimitedExtractor(r);
		extractor.setFieldDelimiter(DELIMITER);
		Arrays.fill(colId, -1);
		formatter = new AprsFormatter();
	}
	
	public void close() throws IOException {
		extractor.close();
	}

	public Object[] read() throws IOException {
		return read(new StationCallCollator());
	}

	public Object[] read(Comparator comp) throws IOException {
		TreeSet stations = new TreeSet(comp);
		ArrayList matrix = extractor.extractRectangularData();
		getColumnIds((ArrayList)matrix.get(0));
		final int ceil = matrix.size();
		for (int idx = 1; idx < ceil; idx ++) {
			Station astation = createStation(
				(ArrayList)matrix.get(idx), (idx + 1));
			if (astation != null) {
				stations.add(astation);
			}
		}
		return stations.toArray();
	}

	/**
	  * Find in COL_NAMES the index that corresponds to the given name.
	  *
	*/

	public static final int findColumnNameIdx(final String name) {
		for(int idx = 0; idx < COL_NAMES.length; idx ++) {
			if (name.equals(COL_NAMES[idx])) return idx;
		}
		return -1;
	}

	///////////////// protected below ////////////////////////////

		/**
		  * Create a station object from values in a line of input.
		  * @param data values from line of input.
		  * @param the index of the row, used for printing 
		  *  error message.
		  *
		*/

	protected Station createStation(ArrayList data, int rowidx) {
		final int rownum = rowidx + 1; 	// for error messeages
		boolean ok = true;
		Station answer = new Station();

		String value = getStringValue(data, IDX_CALL_SGN);
		if (value != null && value.length() > 0) {
			answer.setCallsign(value);
		}

		value = getStringValue(data, IDX_NAME);
		if (value != null && value.length() > 0) {
			answer.setName(value);
		}

		value = getStringValue(data, IDX_ARES);
		if (value != null && value.length() > 0) {
			if ("yes".equals(value)) {
				answer.setIsAres(true);
			} else if ("no".equals(value)) {
				answer.setIsAres(false);
			}
		}

		value = getStringValue(data, IDX_RACES);
		if (value != null && value.length() > 0) {
			if ("yes".equals(value)) {
				answer.setIsRaces(true);
			} else if ("no".equals(value)) {
				answer.setIsAres(false);
			}
		}

		value = getStringValue(data, IDX_CELLPHONE);
		if (value != null && value.length() > 0) {
			answer.setCellphone(value);
		}
 
		value = getStringValue(data, IDX_EMAIL);
		if (value != null && value.length() > 0) {
			answer.setEmail(value);
		}

		value = getStringValue(data, IDX_PRIM_DESCR);
		if (value != null && value.length() > 0) {
			answer.setPrimaryDescription(value);
		}

		value = getStringValue(data, IDX_PRIM_ADDR);
		if (value != null && value.length() > 0) {
			answer.setPrimaryAddress(value);
		}

		value = getStringValue(data, IDX_PRIM_CITY);
		if (value != null && value.length() > 0) {
			answer.setPrimaryCity(value);
		}

		value = getStringValue(data, IDX_PRIM_STATE);
		if (value != null && value.length() > 0) {
			answer.setPrimaryState(value);
		}

		value = getStringValue(data, IDX_PRIM_ZIP);
		if (value != null && value.length() > 0) {
			answer.setPrimaryZip(value);
		}

		value = getStringValue(data, IDX_PRIM_LAT_DEG);
		if (value != null && value.length() > 0) {
		      try {
			answer.setPrimaryLatitude1(parseLatitudeDegrees(value));
		      } catch(Exception e) {
			printError(rowidx,  colId[IDX_PRIM_LAT_DEG], value);
			ok = false;
		      }
		}
 
		value = getStringValue(data, IDX_PRIM_LAT_MIN);
		if (value != null && value.length() > 0) {
		      try {
			answer.setPrimaryLatitude2(parseCoordinateMinutes(value));
		       } catch(Exception e) {
			printError(rowidx,  colId[IDX_PRIM_LAT_MIN], value);
			ok = false;
		       }
		}
 
		value = getStringValue(data, IDX_PRIM_LAT_DIR);
		if (value != null && value.length() > 0) {
		       try {
			char dir = (value).toUpperCase().trim().charAt(0);
			answer.setPrimaryLatitudeOrientation(dir);
		      } catch(Exception e) {
			printError(rowidx,  colId[IDX_PRIM_LAT_DIR], value);
			ok = false;
		      } catch(Error e) {
			printError(rowidx, colId[IDX_PRIM_LAT_DIR], value);
			ok = false;
		      }
		}
 
		value = getStringValue(data, IDX_PRIM_LONG_DEG);
		if (value != null && value.length() > 0) {
		      try {
			answer.setPrimaryLongitude1(parseLongitudeDegrees(value));
		      } catch(Exception e) {
			printError(rowidx,  colId[IDX_PRIM_LONG_DEG], value);
			ok = false;
		      }
		}
 
		value = getStringValue(data, IDX_PRIM_LONG_MIN);
		   if (value != null && value.length() > 0) {
		      try {
			answer.setPrimaryLongitude2(parseCoordinateMinutes(
					value));
		       } catch(Exception e) {
			printError(rowidx,  colId[IDX_PRIM_LONG_MIN], value);
			ok = false;
		       }
		}
 
		value = getStringValue(data, IDX_PRIM_LONG_DIR);
		   if (value != null && value.length() > 0) {
		      try {
			char dir = (value).toUpperCase().trim().charAt(0);
			answer.setPrimaryLongitudeOrientation(dir);
		      } catch(Exception e) {
			printError(rowidx,  colId[IDX_PRIM_LONG_DIR], value);
			ok = false;
		      } catch(Error e) {
			printError(rowidx,  colId[IDX_PRIM_LONG_DIR], value);
			ok = false;
		      }
		}

		value = getStringValue(data, IDX_PRIM_ICON);
		if (value != null && value.length() > 0) {
		   if (AprsFormatter.isValidIcon(value)) {
			answer.setPrimaryIcon(value);
		   } else {
			printError(rowidx,  colId[IDX_PRIM_ICON], value);
			ok = false;
		   }
		}

			// secondary location information

		value = getStringValue(data, IDX_SEC_DESCR);
		if (value != null && value.length() > 0) {
			answer.setSecondaryDescription(value);
		}
 
		value = getStringValue(data, IDX_SEC_ADDR);
		if (value != null && value.length() > 0) {
			answer.setSecondaryAddress(value);
		}
 
		value = getStringValue(data, IDX_SEC_CITY);
		if (value != null && value.length() > 0) {
			answer.setSecondaryCity(value);
		}
 
		value = getStringValue(data, IDX_SEC_STATE);
		if (value != null && value.length() > 0) {
			answer.setSecondaryState(value);
		}
 
		value = getStringValue(data, IDX_SEC_ZIP);
		if (value != null && value.length() > 0) {
			answer.setSecondaryZip(value);
		}
 
		value = getStringValue(data, IDX_SEC_LAT_DEG);
		if (value != null && value.length() > 0) {
		      try {
			answer.setSecondaryLatitude1(parseLatitudeDegrees(value));
		      } catch(Exception e) {
			printError(rowidx, colId[IDX_SEC_LAT_DEG], value);
			ok = false;
		      }
		}
 
		value = getStringValue(data, IDX_SEC_LAT_MIN);
		if (value != null && value.length() > 0) {
		      try {
			answer.setSecondaryLatitude2(parseCoordinateMinutes(
					value));
		       } catch(Exception e) {
			printError(rowidx,  colId[IDX_SEC_LAT_MIN], value);
			ok = false;
		       }
		}
 
		value = getStringValue(data, IDX_SEC_LAT_DIR);
		if (value != null && value.length() > 0) {
		      try {
			char dir = (value).toUpperCase().trim().charAt(0);
			answer.setSecondaryLatitudeOrientation(dir);
		      } catch(Exception e) {
			printError(rowidx,  colId[IDX_SEC_LAT_DIR], value);
			ok = false;
		      } catch(Error e) {
			printError(rowidx,  colId[IDX_SEC_LAT_DIR], value);
			ok = false;
		      }
		}
 
		value = getStringValue(data, IDX_SEC_LONG_DEG);
		if (value != null && value.length() > 0) {
		      try {
			answer.setSecondaryLongitude1(parseLongitudeDegrees(
				value));
		      } catch(Exception e) {
			printError(rowidx,  colId[IDX_SEC_LONG_DEG], value);
			ok = false;
		      }
		}
 
		value = getStringValue(data, IDX_SEC_LONG_MIN);
		if (value != null && value.length() > 0) {
		      try {
			answer.setSecondaryLongitude2(parseCoordinateMinutes(
					value));
		       } catch(Exception e) {
			printError(rowidx,  colId[IDX_SEC_LONG_MIN], value);
			ok = false;
		       }
		}
 
		value = getStringValue(data, IDX_SEC_LONG_DIR);
		if (value != null && value.length() > 0) {
		     try {
			char dir = (value).toUpperCase().trim().charAt(0);
			answer.setSecondaryLongitudeOrientation(dir);
		     } catch(Exception e) {
			printError(rowidx,  colId[IDX_SEC_LONG_DIR], value);
			ok = false;
		     } catch(Error e) {
			printError(rowidx,  colId[IDX_SEC_LONG_DIR], value);
			ok = false;
		     }
		}

		value = getStringValue(data, IDX_SEC_ICON);
		if (value != null && value.length() > 0) {
			if (AprsFormatter.isValidIcon(value)) {
				answer.setSecondaryIcon(value);
			} else {
				printError(rowidx,  colId[IDX_SEC_ICON], value);
				ok = false;
			}
		}

                // Need to set the comment to something even if there is no
                // comment in the DB, to prevent a null field from being sent
                // to the ObjectXmitter. -cshields 12/24/08
		value = getStringValue(data, IDX_COMMENT);
		if (value != null && value.length() > 0) {
			answer.setComment(value);
		} else {
                        answer.setComment("");
                }

		return ok ? answer : null;
	}


	/**
	  * Assign columns in the input data to fields in the 
	  * Station objects.  The column number in the input is
	  * assigned to the element in colId that corresponds to
	  * the field. 
	  *
	*/

	protected void getColumnIds(ArrayList names) {
		final int ceil = names.size();
		for (int idx = 0; idx < ceil; idx ++) {
			String fieldname = (String)names.get(idx);
			int nameidx = findColumnNameIdx(fieldname);
			if (nameidx > -1) {
				colId[nameidx] = idx;
			}
		}
	}
 
		/** Find the name of a column in the input stream. */

	protected String getInputColumnName(final int inputcol) {
		final int ceil = colId.length;
		for(int idx = 0; idx < ceil; idx ++) {
			if (inputcol == colId[idx]) {
				return COL_NAMES[idx];
			}
		}
		return "not defined";
	}
 
		/** Get the value from the ArrayList that corresponds to
 		    the given field id (e.g., IDX_CALL_SGN). */

	protected String getStringValue(ArrayList data, int colidx) {
		String answer = null;
		int inputcol = colId[colidx];
		if (inputcol > -1) {
			answer = (String)(data.get(inputcol));
		}
		return answer;
	}

	protected float parseCoordinateMinutes(String string)
						throws ParseException {
		return new Double(formatter.parseCoordinateMinutes(string))
				.floatValue();
	}

	protected int parseLatitudeDegrees(String string) throws ParseException {
		return formatter.parseCoordinateLatitudeDegrees(string);
	}

	protected int parseLongitudeDegrees(String string) throws ParseException {
		return formatter.parseCoordinateLongitudeDegrees(string);
	}

	/**
	  * Print an error message for a data value that could not be
	  * parsed or that is innappropriate.
	  * @param linenum - line number in input stream
 	  * @param fieldid - the index of the field in colId (or COL_NAMES)
	  *			for which the error occured.
	  * @param value - the bad value that could not be parsed.
	  *
	*/

	protected void printError(int linenum, int fieldid, String value) {
		ERROR_DATA[0] = new Integer(linenum).toString();
		ERROR_DATA[1] = getInputColumnName(fieldid);
		ERROR_DATA[2] = value;
		System.err.println(MessageFormat.format(ERROR_MSG, ERROR_DATA));
	}

}
